(ns jumpluff.client
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.commands :refer [commands commands-via-mention]]
            [jumpluff.state :as state]
            [jumpluff.poi-message :as poi-message]
            [jumpluff.utils :refer [promise-to-chan log]])
  (:require-macros [cljs.core.async.macros :as async-macros]))

(def discord (js/require "discord.js"))

(defn- once-sent
  "Start the reaction collector on sent responses to `command-name`"
  [sent-message src-message command-name]
  (when poi-message/*enable-reactions*
    (when (some #{command-name} ["poi" :inline])
      (poi-message/poi-once-sent sent-message src-message))))

(defn- maybe-respond
  "Respond to `message` when `command-result` is a string or richembed object.
  When `command-result` is an async/chan recur with the taken value.
  On error when sending the message, retry once if it was a dns lookup error"
  [message cb command-result]
  (cond (= (type command-result)
           cljs.core.async.impl.channels/ManyToManyChannel)
        (async-macros/go
          (maybe-respond message cb (async/<! command-result)))
        (not (or (boolean? command-result) ; handle embeds or strings
                 (nil? command-result)))
        (async-macros/go
          (when-let [sent-message (->> message.channel
                                     (promise-to-chan #(.send % command-result))
                                     async/<!)]
            (cb sent-message)))))

(defn- guild-admin?
  "When `message`.member is non-nil, check for admin permission.
  message.member will be nil when the message originated from a DM"
  [message]
  (some-> message.member
          (.hasPermission "ADMINISTRATOR")))

(defn- cmd
  "Invoke and respond to COMMAND-NAME if the permission is correct via MESSAGE."
  [message command-name argument-str]
  (when-let [command (get commands command-name)]
    (when (cond (= :admin (:privilege command))
                (guild-admin? message)
                (= :open (:privilege command))
                true)
      (let [argument (s/trimr (or argument-str ""))]
        (if-not (= command-name :inline)
          (log "—Invoking '" command-name "' '" argument "'"))
        ((:action command) message argument)))))

(defn- on-message
  [client]
  (fn [message]
    (if (not-any? nil? [message message.content])
      (do
        (log (if (nil? message.guild)
               message.channel.type
               (str message.guild.nameAcronym "#" message.channel.name))
             "|" message.author.tag
             (when (guild-admin? message)
               "(admin)") ": "
             (or (not-empty message.content)
                 (str "(embeds:" (count message.embeds) ")")))
        (when (and (not= message.author.id client.user.id)
                   (not message.author.bot))
          (let [mentioned? (.isMentioned message client.user)
                content (if mentioned?
                          (-> message.cleanContent
                             (s/replace (str "@" client.user.username) "")
                             (s/replace #"\s{2,}" " "))
                          message.content)
                [first-word rest-str] (-> content
                                         s/triml
                                         (s/split #" " 2))
                [command-name command-argument]
                (if mentioned?
                  (let [first-word-lowercase (s/lower-case first-word)]
                    (when (some #{first-word-lowercase} commands-via-mention)
                      [first-word-lowercase rest-str]))
                  (if (some #{first-word} state/poi-query-prefixes)
                    ["poi" rest-str]
                    [:inline]))]
            (when (not (nil? command-name))
              (some->> (cmd message command-name command-argument)
                       (maybe-respond message
                                      #(once-sent % message command-name)))))))
      (console.error message))))

(defn client-start
  [token]
  (let [client (discord.Client.)]
    (doto client
      (.on "ready" #(log "Serving " (count (.array client.guilds)) " server(s) as " client.user.tag))
      (.on "message" (on-message client))
      (.on "disconnected" #((log "disconnected, attempting to re-log in")
                            (.login client token)))
      (.on "error" console.error)
      (.login token))))

(defn client-finish
  [client]
  (async-macros/go
    (async/<! (poi-message/cleanup-msg-map))
    (async/<! (promise-to-chan (.destroy client)))))
